Free tools Windows power users keep installed
One-click scans. No signup required.
iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
Antihost offers VPS plans with one-click Docker, NVMe storage and a 14-day free trial.
The production-ready pattern is a three-service Docker Compose stack: Flask served by Gunicorn, MongoDB on the private Compose network, and NGINX as the only public entry point. Inject the MongoDB URI and credentials at runtime, store database files in a persistent volume, terminate HTTPS at the proxy, and keep independent backups. This arrangement answers the common deployment problems—container-to-container connectivity, data loss during redeployments, and exposing Flask’s development server to the internet.
What the finished deployment looks like
A browser connects to ports 80 or 443 on NGINX. NGINX forwards the request over the internal Compose network to the Flask container on port 8000. Flask uses the Compose service name mongo as the database hostname, so it never needs the VPS public IP. MongoDB listens only on the private Docker network and writes its files to a named volume mounted at /data/db.
Internet
|
80/443 on NGINX
|
internal network: nginx -> web:8000 -> mongo:27017
|
persistent mongo-data volume
The separation is deliberate. Gunicorn handles WSGI application processes, NGINX handles public HTTP and TLS concerns, and MongoDB remains unreachable from the public interface unless you deliberately publish its port. Docker’s official samples use the same NGINX, Flask, and MongoDB topology, while Flask’s deployment guidance says:
Do not use the development server when deploying to production.
Prerequisites on the VPS
- A VPS with a supported Linux distribution, a deployment user that is not root, and enough disk space for the operating system, images, logs, and MongoDB data.
- A DNS A or AAAA record pointing your application hostname to the VPS address.
- Docker Engine and the Docker Compose plugin installed from the distribution or Docker’s supported installation method. Confirm with
docker --versionanddocker compose version. - A host firewall allowing SSH and the web ports used by the proxy. Do not open port 27017 to the internet.
- SSH access using a key, regular operating-system updates, and a plan for unattended security patches or scheduled maintenance.
Installation commands differ between Debian-based, Red Hat-based, and other distributions. Follow the instructions for the exact operating system image supplied by your VPS provider, then verify that the deployment account can run Docker without switching to a root shell for every command.
Create a small Flask application
Use a health endpoint that checks the application process and a database operation separately. Keeping the checks distinct makes it clear whether a failure is in Flask, the network, or MongoDB.
Suggested project layout
flask-mongo-vps/
├── app.py
├── requirements.txt
├── Dockerfile
├── compose.yaml
├── nginx.conf
├── .env
└── .dockerignore
Application code
The following example uses PyMongo. The URI is read from the environment; it is not embedded in the image or committed to source control.
import os
from flask import Flask, jsonify
from pymongo import MongoClient
def create_app():
app = Flask(__name__)
uri = os.environ['MONGODB_URI']
client = MongoClient(uri, serverSelectionTimeoutMS=5000)
database = client[os.environ['MONGODB_DATABASE']]
@app.get('/health')
def health():
client.admin.command('ping')
return jsonify(status='ok')
@app.get('/')
def index():
return jsonify(service='flask', database=database.name)
return app
app = create_app()
A real application should handle connection exceptions and return an appropriate failure response instead of exposing a traceback. The short timeout prevents a request from hanging indefinitely when MongoDB is unavailable. Do not return credentials, connection strings, or internal exception details in an HTTP response.
Pin Python dependencies
Record the versions you have tested in requirements.txt. The exact versions depend on your application and Python base image; the important production practice is that a rebuild resolves the same dependency set instead of silently taking a newer release.
Flask==3.1.0
pymongo==4.10.1
gunicorn==23.0.0
Review these pins before deployment and update them through a controlled change. If your project already uses a lock file or a dependency management tool, build from that lock file instead of maintaining two competing sources of truth.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
Build a production image
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN useradd --create-home --shell /usr/sbin/nologin appuser
USER appuser
EXPOSE 8000
CMD gunicorn --bind 0.0.0.0:8000 --workers 2 --access-logfile - app:app
The image binds Gunicorn to all interfaces inside the container because the proxy reaches it through the container network. That does not publish the port on the VPS; publication is controlled by the Compose file. Two workers are only an example starting point, not a universal capacity recommendation. Tune worker count after observing CPU, memory, request latency, and workload characteristics.
Keep secrets in runtime configuration
Create a private .env file on the VPS and exclude it from version control. Use a long, unique password and URL-encode reserved characters in a MongoDB URI. For example:
MONGO_ROOT_USER=admin
MONGO_ROOT_PASSWORD=replace-with-a-long-random-secret
MONGODB_DATABASE=appdb
MONGODB_URI=mongodb://admin:replace-with-a-long-random-secret@mongo:27017/appdb?authSource=admin
The URI must use mongo, the Compose service name, rather than localhost. Inside the web container, localhost means the Flask container itself. The authSource=admin parameter is needed when the user is created in MongoDB’s administrative database, as in the root-user initialization shown below.
Set restrictive permissions, for example with chmod 600 .env, and ensure your source-control ignore file contains .env, database dumps, and private TLS material. For a larger team or a host with a secret-management facility, use Docker secrets or the provider’s secret store instead of placing plaintext credentials in a general-purpose environment file. Never bake secrets into a Dockerfile layer: image layers can remain available in registries and caches after the source file is deleted.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsDefine the Compose services
Save this as compose.yaml. It is a topology example, not a universal production manifest. Pin image tags or digests after testing, add health checks appropriate to your versions, and mount your actual NGINX configuration and certificate paths.
services:
web:
build: .
env_file:
- .env
expose:
- 8000
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:8.0
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
volumes:
- mongo-data:/data/db
restart: unless-stopped
nginx:
image: nginx:1.27-alpine
ports:
- 80:80
- 443:443
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- web
restart: unless-stopped
volumes:
mongo-data:
Why only NGINX publishes ports
expose makes port 8000 available to other Compose services without binding it to a host interface. The mongo service has no ports entry at all, so a remote client cannot connect directly through the VPS address. Publishing MongoDB for convenience is a common and serious exposure mistake.
Understand startup ordering
depends_on starts MongoDB before the web container, but a started container is not necessarily ready to accept authenticated connections. Add a MongoDB health check and a readiness-aware application retry strategy when startup races matter. Your Flask code should tolerate a temporary database outage rather than failing permanently on the first connection attempt.
Configure NGINX as the reverse proxy
Create nginx.conf with the public hostname substituted for app.example.com:
Recommended Free Tools
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://web:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Initially this serves HTTP so that DNS and routing can be tested. After certificates are installed, add a TLS server on port 443 and redirect port 80 to HTTPS. Preserve the forwarding headers so Flask and extensions can determine the original scheme and client address. If your application generates secure URLs or enforces HTTPS, configure Flask’s proxy handling only for the trusted proxy hops in front of it; never trust arbitrary client-supplied forwarding headers.
Deploy the stack
- Log in as the deployment user and copy the project files to a private directory on the VPS.
- Create
.env, generate a unique MongoDB password, and set the hostname innginx.conf. Check that the file is not readable by unrelated users. - Build the Flask image with
docker compose build. Resolve dependency or Dockerfile errors before starting production services. - Start the stack in the background with
docker compose up -d. - Inspect service state using
docker compose ps. A container that repeatedly restarts needs log inspection rather than repeated restarts. - Read application and proxy logs with
docker compose logs --tail=200 web nginx mongo. Remove secrets from any diagnostic output before sharing it. - From the VPS, test the internal route with
curl http://127.0.0.1/healthonly if NGINX is bound locally and the request host matches your server configuration. From another machine, request the public hostname and verify that the response came through the proxy. - Confirm that the application can perform the MongoDB ping and that the expected database name is returned by the sample endpoint.
Do not treat a successful container start as a successful deployment. Verify DNS, the proxy path, the health endpoint, database authentication, and a representative application request.
Add HTTPS at the edge
Use NGINX, Caddy, or a cloud load balancer to obtain and renew certificates. The proxy should terminate TLS, redirect plain HTTP to HTTPS, and forward only the headers your application needs. Keep certificate files outside the image and mount them read-only. If certificate automation runs on the host, ensure renewal reloads NGINX without deleting the MongoDB volume or interrupting unrelated services.
At minimum, the HTTPS virtual host should listen on 443 with the certificate and key, set the same proxy headers as the HTTP block, and redirect port 80. Test both the redirect and certificate renewal before declaring the deployment complete. Flask’s documentation describes the reverse-proxy role and the adjustments required when the application is behind one.
Make MongoDB data survive redeployments
The named volume mongo-data is the durable boundary for the database files. Recreating the MongoDB container does not remove that volume, so an image update or ordinary docker compose up -d does not erase the database. Data written only to a container’s writable layer is not dependable storage because containers are routinely replaced.
Do not run docker compose down -v as routine cleanup: the -v option removes named volumes. Before destructive maintenance, confirm the volume name with docker volume ls and keep an independent backup.
Back up and restore deliberately
A volume is not a backup. Store encrypted dumps or filesystem snapshots outside the VPS, keep more than one recovery point, and test a restore on a separate instance. A logical dump can be produced from the running container with a command shaped like:
mkdir -p backups
docker compose exec -T mongo mongodump --archive --gzip --authenticationDatabase admin --username $MONGO_ROOT_USER --password $MONGO_ROOT_PASSWORD > backups/mongo-$(date +%F).archive.gz
Load the variables into the shell or substitute them through your secret manager; do not put the password in shell history. Restore into an isolated MongoDB instance first, validate collections and indexes, and record how long the process takes. A backup that has never been restored is an assumption, not a recovery plan.
Production Compose practices
- Maintain separate development and production Compose settings. Development bind mounts and auto-reload behavior are convenient locally but create accidental code exposure and unpredictable deployments on a VPS.
- Keep application source inside the built image for production. Rebuild when code changes instead of mounting the working tree over the image.
- Publish ports only on the reverse proxy. Use the host firewall as a second layer, not as a substitute for correct Compose networking.
- Use
restart: unless-stoppedor an equivalent supervised policy, while remembering that restart behavior cannot fix a bad configuration or exhausted disk. - Pin base images and dependencies, scan images, and schedule updates. Rebuild and test before replacing a running service.
- Set log rotation so container logs cannot fill the VPS disk. Monitor memory, CPU, filesystem usage, MongoDB health, and HTTP error rates.
- Run the application as a non-root user in the image. Give the deployment account only the Docker permissions it needs and protect the Docker socket because access to it is effectively host-level access.
- Rotate MongoDB credentials and TLS keys. Update the URI in the runtime secret and recreate the web service after a credential change.
Update Flask without deleting MongoDB
- Commit and review the application change, dependency pins, and migration plan.
- Build the new web image with
docker compose build web. - Run any database migration or compatibility step while the old application remains able to read the data.
- Recreate only the changed web service with
docker compose up --no-deps -d web. Docker documents this pattern for updating one service without unnecessarily recreating its dependencies. - Check
docker compose ps, inspect the new web logs, and call the health endpoint through NGINX. - Keep the previous image available until the new version is verified. If the update fails, roll back the web image and investigate before touching the MongoDB volume.
This approach is a recreate deployment, not automatic zero downtime. Requests may see a brief interruption while the web container is replaced. For strict availability requirements, run multiple web replicas behind a proxy or move the edge and application scheduling to infrastructure that supports rolling updates.
Troubleshoot by symptom
The web container cannot connect to MongoDB
- Check that
MONGODB_URIusesmongoas the hostname and the correct database and authentication source. - Run
docker compose psand inspect MongoDB logs for initialization or authentication errors. - Confirm that the root username and password in the MongoDB service match the URI. MongoDB initialization variables are used when the data directory is first initialized; changing them later does not rewrite existing users.
- Allow for startup readiness. Add retry logic or a health-aware dependency rather than assuming container start means database readiness.
The browser returns 502 Bad Gateway
- Read NGINX and web logs together. A 502 usually means NGINX cannot reach the upstream or the upstream process closed the connection.
- Confirm Gunicorn listens on
0.0.0.0:8000, not only on container-local loopback. - Check that the proxy target is
web:8000, matching the Compose service name and exposed port. - Verify that the Flask process did not exit during import because an environment variable is missing.
The site works on the VPS but not from the internet
- Verify the DNS record and allow ports 80 and 443 in both the VPS firewall and any provider-level firewall.
- Ensure only NGINX publishes those ports and that the proxy is listening on the expected interfaces.
- Check the hostname in the NGINX
server_namedirective and test with the same hostname, not only an IP address.
Data appears to have vanished
- List volumes and inspect the Compose project name. Starting the stack from a different directory can create a different project-scoped volume.
- Check that the MongoDB service still mounts
mongo-data:/data/db. - Look for accidental use of
docker compose down -vor manual volume deletion. - Restore the newest verified external backup if the volume was removed or corrupted. Do not keep experimenting on the only copy.
Self-hosted MongoDB versus a managed MongoDB service
A MongoDB container is appropriate when you accept responsibility for patching, storage, backups, monitoring, and recovery. A managed service reduces much of that operational work but adds recurring service charges, provider-specific networking, and a dependency on the provider’s availability and controls.
Best Value
| Decision area | MongoDB container on the VPS | Managed MongoDB service |
|---|---|---|
| Operational ownership | You patch the image and host, monitor the process, and handle failover. | The provider handles much of the database infrastructure; you still manage users, network access, and application compatibility. |
| Backups and restore | You must schedule, copy, encrypt, and test dumps or snapshots. | Backup features and retention depend on the selected service; verify restore procedures and export options. |
| Latency | Local container traffic usually avoids a network hop between application and database. | Latency depends on region, private networking, and the provider’s placement. |
| Scaling | You resize the VPS or design replication and storage yourself. | Scaling controls may be available, but limits and pricing are provider-specific. |
| Security management | You protect the host, firewall, credentials, TLS, and database exposure. | The provider secures the managed control plane, while you configure identities, allowlists, encryption settings, and application secrets. |
| Recurring cost | Included within the VPS resources, with backup storage and larger instances adding cost. | A separate recurring database charge; no universal price applies across providers or regions. |
Choose self-hosting for a small, stateful application when you can operate the database consistently and have tested backups. Choose a managed service when database maintenance, high availability, geographic distribution, or recovery objectives exceed what one VPS can responsibly provide.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Choosing the reverse-proxy layer
| Option | Strengths | Trade-offs |
|---|---|---|
| NGINX | Widely documented, explicit proxy controls, mature TLS and caching features, and a natural fit for the Compose topology above. | Certificate automation and configuration validation require deliberate setup. |
| Caddy | Simple configuration and automatic HTTPS can reduce certificate-management work. | You still need to understand DNS, renewal permissions, headers, and failure handling. |
| Cloud load balancer | Managed TLS, health checks, observability, and failover can reduce host-level operations. | Additional recurring cost, provider-specific configuration, and another network boundary. |
All three can front Gunicorn. The right choice depends on whether you value local configuration control, automatic certificate management, or managed failover more than minimizing moving parts and cost.
VPS capacity and operating cost
There is no responsible universal VPS size or monthly price for this stack. Required CPU and memory depend on request volume, worker count, response size, background jobs, MongoDB indexes, and log retention. Start with a small instance only when the workload is modest, then watch memory pressure, swap activity, disk growth, and database latency. Reserve separate capacity for backups and image layers so a full disk does not stop MongoDB.
Budget for more than the base VPS: encrypted backup storage, snapshot retention, domain and certificate operations where applicable, monitoring, and a managed database service if you choose not to operate MongoDB yourself. Document who receives alerts and who can restore the service; an inexpensive server without an owner and tested recovery path is not a production plan.
Final deployment checklist
- Flask runs behind Gunicorn;
flask runand the debugger are disabled. - NGINX or another proxy is the only service with public application ports.
- The web container reaches MongoDB through the Compose service name, not a public address.
- Credentials and the URI are injected at runtime and excluded from images and source control.
- MongoDB uses a named or carefully managed host-mounted volume at its data directory.
- Backups are stored independently, encrypted where appropriate, and restored in a test environment.
- HTTPS, forwarding headers, HTTP-to-HTTPS redirection, and certificate renewal have been tested.
- Production Compose settings remove development bind mounts and include restart, logging, and health practices.
- Host, images, Python dependencies, and credentials have an update and rotation schedule.
- You know how to roll back the web image without deleting the MongoDB volume.
With these controls in place, Docker Compose provides a clear single-VPS deployment: NGINX handles the public edge, Gunicorn serves Flask, MongoDB stays private, and the database survives ordinary container replacement through its persistent volume and independent backup plan.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

