A useful docker compose example should make every dependency explicit.
The architecture
Nginx accepts HTTPS on the host loopback interface and forwards requests to Flask.
The complete Compose file
Create a new project directory containing compose.yaml, the four files under flask/ shown below and nginx/default.conf. The setup commands create the remaining secret and certificate files. Use a POSIX shell with OpenSSL available, such as Linux, macOS or a suitable WSL distribution.
services:
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
- "127.0.0.1:8443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
flask:
condition: service_healthy
networks: [public, private]
flask:
build: ./flask
restart: unless-stopped
env_file: ./flask/dev.env
secrets: [db_password]
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=3)"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
networks: [private]
postgres:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 5s
retries: 10
networks: [private]
networks:
public:
private:
internal: true
volumes:
pg-data:
secrets:
db_password:
file: ./secrets/db-password.txtSupporting application files
Create flask/ and nginx/ before saving these files. Dependency ranges keep the demonstration understandable; a production build should use a reviewed lock file and pinned image digests.
Flask>=3.0,<4
gunicorn>=23,<24
psycopg[binary]>=3.2,<4FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN useradd --create-home appuser
COPY app.py .
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app:app"]DATABASE_HOST=postgres
DATABASE_NAME=app
DATABASE_USER=app
DATABASE_PASSWORD_FILE=/run/secrets/db_passwordimport os
from pathlib import Path
import psycopg
from flask import Flask
app = Flask(__name__)
def database_name():
password = Path(os.environ["DATABASE_PASSWORD_FILE"]).read_text().strip()
with psycopg.connect(
host=os.environ["DATABASE_HOST"],
dbname=os.environ["DATABASE_NAME"],
user=os.environ["DATABASE_USER"],
password=password,
connect_timeout=3,
) as connection:
with connection.cursor() as cursor:
cursor.execute("SELECT current_database()")
return cursor.fetchone()[0]
@app.get("/")
def index():
return {"service": "flask", "database": database_name()}
@app.get("/health")
def health():
try:
database_name()
return {"status": "ready"}
except psycopg.Error:
return {"status": "unavailable"}, 503server {
listen 80;
server_name localhost;
return 301 https://localhost:8443$request_uri;
}
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/nginx/certs/localhost.crt;
ssl_certificate_key /etc/nginx/certs/localhost.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://flask:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Prepare local credentials and certificates
Run these commands from the project directory.
mkdir -p flask nginx secrets certs
chmod 700 secrets
openssl rand -hex 32 > secrets/db-password.txt
chmod 444 secrets/db-password.txt
openssl req -x509 -newkey rsa:2048 -nodes -days 30 \
-keyout certs/localhost.key -out certs/localhost.crt \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
chmod 600 certs/localhost.keyExclude secrets/ and private certificate keys from version control. Changing the password file does not rotate the credentials of an already initialised PostgreSQL volume; update the database role deliberately instead. For public HTTPS, obtain a trusted certificate for the actual hostname and implement renewal and proxy reload procedures.
Design decisions explained
Readiness: Flask verifies database connectivity in its health endpoint; Nginx starts after that check succeeds. A PostgreSQL readiness probe establishes availability, while the application probe additionally exercises its configured credentials.
Persistence: the named volume survives normal container replacement. It is not a backup, and docker compose down --volumes destroys its data. Practise restoration before storing anything important.
Access boundaries: the private network has no ordinary external connectivity, while only the proxy publishes loopback ports. Local secret mounts do not automatically encrypt their host-side source files.
Run and verify the stack
docker compose config --quiet
docker compose up -d --build --wait
docker compose ps
curl --cacert certs/localhost.crt https://localhost:8443/
docker compose logs --tail=50 flask
docker compose downThe HTTPS request should return JSON identifying the Flask service and the app database. If it fails, inspect health status and service logs before changing configuration. The certificate flag trusts only the certificate created for this demonstration; do not disable TLS verification as a general troubleshooting habit.
Separate demonstration from production assurance
This example supplies the application files omitted by many configuration sketches, but deployment assurance still requires reviewed dependencies, backup restoration, credential rotation, capacity planning and an appropriate network threat model. It has been checked for internal documentation consistency here; it has not been executed against Docker Engine in this editing environment.
Follow one request through the application
In this docker compose example, Nginx is the entry point, Flask handles application logic and PostgreSQL stores records.
Test each boundary separately when a request fails. An Nginx error can mean the application is unavailable, but it does not by itself explain whether the cause is a wrong upstream name, a crashed Flask process or an application blocked on the database. Read the proxy and application logs for the same request. If Flask reports an authentication failure, continue with the database configuration rather than changing Nginx. Following the earliest specific error keeps a three-service stack understandable and avoids treating every browser error as a proxy problem.
Treat initialisation and migrations separately
The database image can initialise an empty data directory, but an application also needs a strategy for schema changes over time. A new database and an existing database are different test cases. Verify that a clean environment can create the required schema, then verify that a later application version can work with stored records. Use a deliberate migration step instead of depending on an undocumented manual command that someone once ran inside a container. The migration should target the intended database and report a useful failure if it cannot connect.
Be careful when editing initialisation credentials after a named volume already exists. Many database image environment variables affect the first initialisation rather than rewriting an existing database's users and passwords. If the application starts failing after a credential edit, determine whether the stored database account still uses the original password. Use a supported password change procedure for persistent environments. Removing a development volume can produce a fresh database, but it also destroys its records and should only be part of an explicitly disposable test.
Verify persistence with a real application action
A useful persistence check writes a record through the application, recreates the relevant containers and reads the same record again. That confirms more than simply seeing a volume declaration in YAML. It demonstrates that PostgreSQL writes to the intended mounted location and that the application reconnects to the same database afterward. Keep this check small and repeatable so that it can be rerun after a storage configuration change. Use disposable sample records and avoid confusing application seed data with records that actually survived recreation.
A named volume still needs a backup and restore plan if the records matter. Confirm the database's supported backup method and test restoring into a separate environment. Copying files from a running database without understanding consistency requirements may not produce a usable backup. Also document which command removes the project's persistent storage. The difference between replacing a container and deleting a volume is central to operating this example safely. Once a team starts using the stack for shared testing, storage deserves the same review as application code.
Move from demonstration to an owned deployment
Before using the example on a server, choose image versions deliberately and review how updates will be tested. Confirm the application's process server, timeout behaviour and shutdown handling. Keep TLS material and private credentials out of the repository and decide who renews certificates. Limit published ports to the interfaces and services that require them. A local example can explain the architecture, but the person operating the deployment still needs to own access control, monitoring, backups and recovery. Those responsibilities are not supplied by adding another service to the file.
Frequently asked questions
Why place Nginx in front of Flask?
It provides a separate entry point for proxying requests and handling concerns assigned to the web layer. The exact responsibilities depend on your configuration. Flask still needs an application serving setup suitable for the environment in which you run it.
Should PostgreSQL publish port 5432 to my computer?
Only when a host-side client needs direct access. Flask can normally reach PostgreSQL through its service name on the project network. Keeping the database internal avoids an unnecessary host port and makes the request path easier to inspect.
Why does a new database password not work?
An existing data volume may contain a database initialised with earlier credentials. Check the database's account state and use its supported password update procedure. Do not remove valuable storage merely to repeat the initialisation process.
How do I know the example really works?
Make a request through Nginx that exercises Flask and PostgreSQL, inspect service status and logs, then test persistence through container recreation. A proxy welcome page alone does not prove that the application and database are connected correctly.
Conclusion
This Nginx, Flask and PostgreSQL docker compose example is most useful when you understand the request path and the state each service owns. Validate configuration, test database readiness, run migrations deliberately and verify persistence with a real record. Keep local demonstration settings distinct from the responsibilities of a maintained deployment. That gives you a clear foundation for extending the stack without losing track of networking, credentials or database recovery.



