Containerized inventory application for the Octopus exercise. The stack uses Node.js, MongoDB, and Nginx, with each component running in its own container.
The original requirement was a small web application that reads fruit inventory data from MongoDB and shows the number of apples on an HTML page. I extended that base requirement into a fuller demo with four main improvements:
-
Inventory management The app now displays all fruit records, not only apples, and lets the user update quantities through the web UI.
-
Backup and restore The project includes both UI-based and script-based MongoDB backup and restore flows. Backups are written to the host
backups/directory so they remain available outside the containers. -
EC2 startup and deployment The application is deployed to EC2 through GitHub Actions over SSH. On the server side, the stack can be started automatically on reboot with a
systemdservice such as/etc/systemd/system/octopus-app.service. -
Basic password lifecycle separation MongoDB responsibilities are separated by purpose. The Mongo admin user is used for database initialization, deployment, and user management, while the application itself connects with the application database user. Backup and restore use the same application user, which keeps the design simple while still ensuring that the Mongo admin credential is not exposed inside the app container.
The application is composed of four containers:
nginx: public entrypoint on port80, acts as a load balancerapp1: Node.js / Express web application (instance 1)app2: Node.js / Express web application (instance 2)mongodb: MongoDB database
Request flow:
Browser -> Nginx -> app1 or app2 (round-robin) -> MongoDB
MongoDB stores its live data in a named Docker volume mounted to /data/db.
- If the containers restart normally, the data is preserved.
- If the EC2 instance reboots and the volume still exists, the saved inventory is preserved.
- The default fruit values from
mongo/init.jsare only applied when MongoDB starts with a fresh empty data directory.
This means the application does not reset to the default inventory on every restart. It only returns to the seeded values when the MongoDB volume is recreated from scratch.
The project includes simple MongoDB operational recovery flows.
- Backups are stored on the host machine under
./backups/. - Backup files are not committed to Git.
- The web UI can create and restore numbered backup archives.
- Shell scripts are also included for manual operational use.
Make sure the stack is running, then run:
./scripts/backup.shRun:
./scripts/restore.sh ./backups/<backup-file>.archive.gzCI/CD is defined in .github/workflows/cicd.yml.
- In CI, the workflow uses
docker composeon the GitHub runner. - On EC2, the deploy step uses
docker-compose, because the target server uses the older Compose binary. - The workflow writes the environment file, starts MongoDB first, ensures the application user exists, and then starts the app and Nginx containers.
For this demo, the sample environment file is intentionally simple.
Example values are provided in .env.example:
MONGO_USERNAME=admin
MONGO_PASSWORD=your-admin-password
MONGO_DB=fruitsdb
APP_PORT=3000
APP_DB_USERNAME=fruits_app
APP_DB_PASSWORD=your-app-passwordThis project is designed as a demo and exercise, not as a full production platform. Even so, one important improvement was made in the current version:
- the Mongo admin credentials are not passed into the application container
- the app connects with the application database user
- deployment and user-management tasks still use the Mongo admin user
This keeps the runtime application less privileged than the deployment flow.
.
├── app
│ ├── Dockerfile
│ ├── package.json
│ └── server.js
├── mongo
│ ├── Dockerfile
│ ├── ensure-app-user.js
│ └── init.js
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
├── scripts
│ ├── backup.sh
│ └── restore.sh
├── .github
│ └── workflows
│ └── cicd.yml
├── .env.example
├── docker-compose.yml
└── README.md
flowchart LR
Browser --> Nginx[nginx container\nload balancer]
Nginx --> App1[node.js app1]
Nginx --> App2[node.js app2]
App1 --> Mongo[(mongodb container)]
App2 --> Mongo