Self-hosted cloud notes with a focused React editor, FastAPI backend, PostgreSQL storage, cookie authentication, image uploads, and English/Russian localization.
- Frontend: React 19, TypeScript, Vite, TipTap, and Lucide icons
- Backend: FastAPI, Pydantic, and async SQLAlchemy 2.0
- Database: PostgreSQL 16
- Authentication: JWT in HttpOnly cookies with bcrypt password hashing
- Web server: Caddy providing automatic HTTPS in front of the internal Nginx service
- Deployment: Docker Compose with health checks and persistent volumes
Docker or OrbStack is the only requirement. Python, Node.js, Nginx, and PostgreSQL do not need to be installed on the host.
Clone the repository:
git clone <repository-url>cd Cloud_Notes_api
Create the environment file and set a public domain:
cp .env.example .env
Set
DOMAINto a DNS name that points to the router's public IP. Forward TCP ports 80 and 443 from the router to the Docker host before starting the stack.Start the complete application with one command:
docker compose up -d --build
Open Cloud Notes:
- Application:
https://notes.example.com - API documentation:
https://notes.example.com/api/docs
- Application:
Caddy obtains and renews the public TLS certificate automatically. PostgreSQL, FastAPI, Nginx, and the Vite build are reachable only through the private Docker network.
Local testing does not require a domain, TLS certificate, router configuration, or a production .env file. Start only PostgreSQL, FastAPI, and Nginx with the local override:
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build db server frontendOpen:
- Application:
http://localhost:8080 - API documentation:
http://localhost:8080/api/docs - Health check:
http://localhost:8080/api/system/health-check
To test from another device on the same LAN, replace localhost with the Docker host's local IP address:
http://SERVER_LAN_IP:8080
The local override sets COOKIE_SECURE=false so authentication works over plain HTTP. It publishes only the Nginx frontend; FastAPI and PostgreSQL remain internal Docker services.
Stop the local stack with:
docker compose -f docker-compose.yml -f docker-compose.local.yml downSet a different local port when 8080 is occupied:
LOCAL_APP_PORT=9090 docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build db server frontendCreate .env before the first start:
cp .env.example .envRecommended settings:
DOMAIN=notes.example.comPOSTGRES_USER=cloud_notesPOSTGRES_PASSWORD=use-a-long-random-passwordPOSTGRES_DB=cloud_notesJWT_SECRET_KEY=use-a-random-secret-at-least-32-bytes-longJWT_EXPIRE_MINUTES=43200SESSION_MAX_AGE_SECONDS=2592000COOKIE_SECURE=trueREGISTRATION_ENABLED=trueSQL_ECHO=falseDOMAINmust resolve to the router's public IP. Dynamic addresses can be maintained with any compatible DDNS provider.JWT_SECRET_KEYkeeps login sessions valid after backend recreation. Without it, a secure temporary key is generated at every backend start and existing sessions are invalidated.JWT_EXPIRE_MINUTEScontrols token lifetime.43200is 30 days;0explicitly enables non-expiring tokens.SESSION_MAX_AGE_SECONDScontrols the browser cookie lifetime and should match the intended token lifetime.COOKIE_SECURE=trueprevents authentication cookies from being sent over plain HTTP.- Set
REGISTRATION_ENABLED=falseafter creating the required accounts if public registration is not needed. - Database credentials must be selected before the PostgreSQL volume is initialized. Changing them later does not modify an existing database volume.
Apply configuration changes with:
docker compose up -d --buildBefore exposing the application to the internet:
Create the local environment file:
cp .env.example .env
Replace every value marked
PRODUCTIONin.env:set a real public
DOMAIN;choose PostgreSQL credentials before the first database start;
generate and preserve a JWT signing key:
openssl rand -hex 32
Configure DNS: point the selected domain to the router's public IP. Configure a DDNS updater if that address changes periodically.
Give the Docker host a fixed LAN address: use a DHCP reservation or a static network configuration.
Configure router forwarding:
- TCP 80 to the Docker host port 80;
- TCP 443 to the Docker host port 443;
- UDP 443 to the Docker host port 443 only when HTTP/3 is wanted.
Do not expose internal services: ports 5432, 8000, and the former development port 8080 must remain closed externally.
Keep secure cookies enabled:
COOKIE_SECURE=trueis required for the HTTPS deployment.Restrict account creation: after creating the intended accounts, consider setting
REGISTRATION_ENABLED=falseand recreating the server container.Verify certificate issuance:
docker compose logs -f caddy
Test from an external network: open the configured HTTPS domain over cellular data instead of the server's local Wi-Fi.
Configure backups: regularly back up both the PostgreSQL data and uploaded attachment volume.
| Service | Container port | Host port | Exposure |
|---|---|---|---|
| Caddy HTTP | 80 | 80/tcp | Public; redirects to HTTPS and handles ACME |
| Caddy HTTPS | 443 | 443/tcp and 443/udp | Public HTTPS and HTTP/3 |
| Nginx frontend | 80 | Not published | Docker network only |
| FastAPI backend | 8000 | Not published | Docker network only |
| PostgreSQL | 5432 | Not published | Docker network only |
Do not forward ports 5432, 8000, or the former frontend port 8080 from the router.
Docker volumes preserve both database records and uploaded images:
pgdatastores PostgreSQL data.uploadsstores note attachments.
On upgrade, files from the legacy backend/uploads directory are copied into the attachment volume automatically. Existing attachment links that used localhost:8000 are normalized by the API.
Stop containers without deleting data:
docker compose downDo not use docker compose down -v unless all notes and uploaded images may be permanently deleted.
Create a database backup:
docker compose exec -T db pg_dump -U admin main_db > cloud-notes-backup.sqlWhen custom PostgreSQL values are configured, replace admin and main_db in the backup command.
Check container health and published ports:
docker compose psFollow application logs:
docker compose logs -f caddy frontend server dbRestart the stack:
docker compose restartUpdate after pulling new code:
git pull
docker compose up -d --buildCloud_Notes_api/
├── backend/
│ ├── app/
│ │ ├── api/ # Notes, users, attachments, and health routes
│ │ ├── database.py # Async SQLAlchemy engine and sessions
│ │ ├── dependencies.py # Authentication dependencies
│ │ ├── main.py # FastAPI application and startup lifecycle
│ │ ├── models.py # User, note, and attachment models
│ │ ├── schemas.py # Request and response validation
│ │ └── utils.py # Password hashing and JWT helpers
│ └── Dockerfile
├── frontend/
│ ├── src/ # React application, styles, API client, and localization
│ ├── Dockerfile # Vite build and Nginx runtime
│ └── nginx.conf # Static frontend and `/api` reverse proxy
├── Caddyfile # Public HTTPS and reverse proxy configuration
├── docker-compose.yml
├── .env.example
└── README.md
- Only Caddy ports 80 and 443 are published.
- Authentication cookies are HttpOnly and use
SameSite=Lax. - Token and cookie lifetimes are configurable. The production example uses 30 days; signing out invalidates the browser cookie immediately.
- Login attempts are limited per client address.
- Uploaded images are size-limited, streamed to disk, and checked by extension, MIME type, and file signature.
- SQL parameter logging is disabled by default.
- The backend container runs as an unprivileged user.
- Caddy redirects HTTP to HTTPS and manages certificate renewal automatically.

