Site ↗
Documentation sections
Operations · 0.8.1

Moving a local database and images to a VPS

Save the dev database and images on a Mac, send them to the server, restore them, and start the application step by step.

On this page

To transfer only updated articles while keeping server users, go to Update documentation only and preserve server users below. The full transfer in the first steps replaces the entire database, including accounts.

This guide moves the database and images from local make dev to one VPS where the application has already deployed successfully. Every step, from creating a dump on a Mac to checking the website, is included below.

The result: the server gets records, content settings, and accounts from the local database. Sign in using the local username and password. Server keys, domains, and Docker settings remain unchanged.

Import replaces server data rather than merging two databases. An additional server copy is unnecessary for a new empty installation. If the server holds needed data, step 5 provides preservation commands.

Before starting

  • provision and first deploy have already succeeded on the VPS. Do not repeat them for import. Do not import between them: control expects a new database on first installation.
  • Dev and server use the same schema and migrations. If the model changed after the server release, release current code normally first.
  • Images use Local files. This guide transfers the entire directory. It does not apply to S3.

example_project below is the technical project name, and production is the server environment name. Replace them if different. Replace IP 203.0.113.10 with your VPS address too. For another development computer or prod-local, see the separate guide.

1. On the Mac: stop the application and find the dev database

Stop make dev with Ctrl+C in its terminal so the application does not change records or images during copying. Stop your own background tasks if they write to this database.

In the Mac terminal, enter the generated project folder:

cd /path/to/example-project
make up
make wait-db
docker ps --format 'table {{.Names}}\t{{.Image}}'

make up starts PostgreSQL without backend or interface, and make wait-db waits for database readiness. Find the PostgreSQL container belonging specifically to the dev project. You need its name in the next block.

umask 077
DEV_POSTGRES=example_project-postgres-1
DEV_MEDIA="$HOME/.local/share/example_project/media"
TRANSFER_DIR="$HOME/example-transfer-$(date +%Y%m%d-%H%M%S)"
mkdir -m 700 "$TRANSFER_DIR"

Replace DEV_POSTGRES with the actual container name. DEV_MEDIA is the Mac image directory; if you used another MEDIA_ROOT, enter that path. TRANSFER_DIR is a new folder for the two transfer files, outside the repository.

Until files are sent, use this same terminal: it holds the variables.

2. On the Mac: save the database and images

Create a dump first:

docker exec "$DEV_POSTGRES" sh -c \
  'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
  > "$TRANSFER_DIR/database.dump"

The command takes the database name and user from the dev container. -Fc creates a PostgreSQL archive for later pg_restore. The dump contains tables, records, and accounts, but not image files.

If the command succeeded, archive the images:

COPYFILE_DISABLE=1 tar -czf "$TRANSFER_DIR/media.tar.gz" -C "$DEV_MEDIA" .

The archive includes media directory contents and prepared sizes. COPYFILE_DISABLE=1 excludes macOS metadata. It matches this specific dump: do not resume dev writes until both commands finish.

If the database has never contained images, no archive is needed: skip subsequent media.tar.gz steps, media directory discovery, and storage binding changes. If images existed but the directory is missing, find it or a previous copy first — a dump alone cannot restore images.

3. On the Mac: send files to the VPS

Specify the user and IP you normally use for server access:

VPS=deploy@203.0.113.10
ssh "$VPS" 'umask 077; mkdir -p "$HOME/example-transfer"'
scp "$TRANSFER_DIR/database.dump" "$TRANSFER_DIR/media.tar.gz" \
  "$VPS:example-transfer/"

For database-only transfer, remove "$TRANSFER_DIR/media.tar.gz" from scp.

Files go into the SSH user's home directory: /home/deploy/example-transfer for deploy, or /root/example-transfer for root. If SSH requires a separate key, add your usual -i /путь/к/ключу to ssh and scp.

After successfully creating and sending the copy, you can restart local make dev. New local changes are not included in the existing dump.

4. On the VPS: stop the application

You now need a VPS terminal as root to change file ownership. Connect normally; if signed in as a sudo user, run sudo -i. If sudo is unavailable, use root access from the VPS panel.

List container names:

docker ps -a --format 'table {{.Names}}\t{{.Status}}'

Enter actual backend, frontend, and PostgreSQL container names for your environment. Do not select another project or environment's containers.

umask 077
TRANSFER_DIR=/home/deploy/example-transfer
TARGET_BACKEND=example_project-production-backend-1
TARGET_FRONTEND=example_project-production-frontend-1
TARGET_POSTGRES=example_project-production-postgres-1
TARGET_DB=app

If files were sent to root, change TRANSFER_DIR to /root/example-transfer. app is the database of an ordinary first installation. If standard restore was previously used, the active database may have changed: this example covers first import after initial deploy, rather than such an environment.

Stop frontend and backend, leaving PostgreSQL running:

docker stop "$TARGET_FRONTEND" "$TARGET_BACKEND"

The application is unavailable from now until step 8. Also stop your own background writers. Run all following server blocks in this same terminal to retain variables.

5. On the VPS: find media storage and preserve old data if needed

The VPS image path may differ from the Mac path. Read it from the existing container configuration:

export BACKEND_MEDIA=$(docker inspect "$TARGET_BACKEND" \
  --format '{{range .Config.Env}}{{println .}}{{end}}' \
  | sed -n 's/^MEDIA_ROOT=//p')

TARGET_MEDIA=$(docker inspect "$TARGET_BACKEND" \
  | jq -er --arg path "$BACKEND_MEDIA" \
    '.[0].Mounts[] | select(.Destination == $path and .Type == "bind") | .Source')

test -n "$BACKEND_MEDIA" && test -d "$TARGET_MEDIA"

BACKEND_MEDIA is the backend's container path. TARGET_MEDIA is the VPS file directory. The final command prints nothing on success. If the path is missing or the command fails, do not extract files: check the backend container name and Local storage.

If the server is empty and its data is unnecessary, go directly to step 6. To preserve current data, run before import:

docker exec "$TARGET_POSTGRES" \
  pg_dump -U postgres -d "$TARGET_DB" -Fc \
  > "$TRANSFER_DIR/server-before.dump"

tar -czf "$TRANSFER_DIR/server-media-before.tar.gz" -C "$TARGET_MEDIA" .

This is a separate manual copy of the previous database and files. It requires neither enabled backup nor S3. Run the second command only if a media directory exists; confirm copying finishes without errors.

6. On the VPS: restore database and files

Restore the database first:

docker exec -i "$TARGET_POSTGRES" \
  pg_restore -U postgres -d "$TARGET_DB" \
  --clean --if-exists --no-owner --no-privileges --single-transaction \
  < "$TRANSFER_DIR/database.dump"

--clean --if-exists replaces existing objects included in the dump. This is not a record merge: application tables and data are restored from dev. --no-owner --no-privileges excludes local ownership and permission assignments. --single-transaction cancels database changes if restoration fails.

Wait for success. If an error occurs, do not continue import; preserve its text. If successful, extract images:

tar -xzf "$TRANSFER_DIR/media.tar.gz" -C "$TARGET_MEDIA"
chown -R 10001:10001 "$TARGET_MEDIA"

The first command restores files. The second assigns them to the backend container user so it can read images and save new ones. Extract directly into the media directory: no extra nested media directory is needed. Existing extra files on disk are not deleted, but imported database records will not reference them.

7. On the VPS: bind images to the new path

The imported database still records the Mac storage path. Starting the backend immediately may produce media storage cannot change while assets exist.

The following block updates the binding to the server backend path. Run it after copying files, rather than instead of copying. Python 3 must be installed on the VPS (included in server preparation). Copy the entire block; no manual calculation is required:

MEDIA_ID=$(python3 - <<'PYTHON'
import hashlib, os, posixpath
root = os.environ['BACKEND_MEDIA']
assert root.startswith('/') and root != '/', 'Проверьте BACKEND_MEDIA'
print(hashlib.sha256(b'local\0' + posixpath.normpath(root).encode() + b'\0').hexdigest())
PYTHON
)

docker exec -i "$TARGET_POSTGRES" \
  psql -U postgres -d "$TARGET_DB" \
  -v ON_ERROR_STOP=1 -v media_id="$MEDIA_ID" <<'SQL'
BEGIN;
SELECT pg_advisory_xact_lock(731947120);
INSERT INTO admingen_media.store_identity (singleton, identity)
VALUES (true, :'media_id')
ON CONFLICT (singleton) DO UPDATE SET identity = EXCLUDED.identity;
COMMIT;
SQL

The block assumes ordinary Local storage in generated deployment, with no symbolic links in the path. Skip this step if images never existed and no archive was transferred.

8. On the VPS: start the application and check results

docker start "$TARGET_BACKEND"
docker logs --tail 50 "$TARGET_BACKEND"
docker exec "$TARGET_BACKEND" migrate --verify

Check that the backend did not exit with an error and migration verification passed. Then start frontend:

docker start "$TARGET_FRONTEND"

Open https://admin.example.com, substituting your domain. Sign in with an account from the local database. The account created on the empty server before import was replaced by dump data; no repeat superadministrator creation is needed.

Open several records and images. If a separate website is used, check that it receives content. Server authorization keys were not transferred from the Mac, so the browser may require a new sign-in.

No image rebuild, provision, or new deploy is required for this import: code in the running release did not change. Preserve dump and archive until validation finishes.

Update documentation only and preserve server users

A full database dump replaces both content and accounts. If server administrators already exist and you only need to transfer articles, do not restore the entire dev dump.

This website project includes two custom scripts for this purpose: deploy/custom/export-docs.py and deploy/custom/import-docs.sh. These transfer this project's documentation; they are not universal generator commands. They update DocArticle and required DocSection by slug and add new articles, but do not delete server records or affect users, roles, images, or other content. Existing server IDs are preserved; selected articles' title, text, order, and publication status come from the local database.

1. Export articles on your computer

From the local admin project root, for example ~/projects/example-project:

python3 deploy/custom/export-docs.py --output docs-update.sql

Docker and the local dev project's PostgreSQL must be running for export. The script accesses its container; add --container ИМЯ_КОНТЕЙНЕРА for a different name. It reads the local dev database and creates an SQL file transferring all documentation, without changing the database. If docs-update.sql exists, choose another name using --output: the script does not overwrite an existing file. For selected articles, specify slugs:

python3 deploy/custom/export-docs.py --output docs-update.sql \
  --slugs docker-images disk-cleanup

Replace slugs with the desired documentation values. The file contains article text rather than an account dump. The recipient imports it next.

2. Send the file and script to the VPS

From the same local project root on your computer:

scp docs-update.sql deploy/custom/import-docs.sh \
  deploy@203.0.113.10:/home/deploy/

Replace IP and user. For a separate SSH key, add -i /путь/к/ключу to scp. This copies the SQL file and import script. Text transfer requires no git pull, build, or new release. Keep custom scripts in Git for later use; SQL need not be published in Git.

3. Import on the server

On the VPS:

bash /home/deploy/import-docs.sh \
  /home/deploy/docs-update.sql \
  /srv/admingen/example_project/production

The first argument is the transferred SQL; the second is the environment runtime directory, rather than the source directory. Find it in the Deployment profile. Run as a user with access to Docker and this environment's files.

Import updates documentation only, in one transaction. On error, transaction changes are not saved. Text changes need no rebuild, release, or container restart. Open an article on the website; if API caching is enabled, wait for its TTL, such as up to 30 seconds when configured that way. Server administrator accounts remain intact.