website
The website for https://crystal-china.org
Development
Development dependencies
- Crystal and Shards
- Lucky CLI
- PostgreSQL client tools
- Bun
- Podman (required by the scripts; Docker compatibility is not guaranteed)
Running locally
-
Install Crystal and the development dependencies above.
-
From the project root, start PostgreSQL with PGroonga and initialize the app:
script/deps.sh crystal run script/setup.cr -
Run
lucky devto start the app
Deployment
No local Crystal, Shards or Bun installation is needed for deployment only; Podman runs them all in containers. Basic Linux tools and cross-architecture emulation are still required as shown below.
Frontend assets are embedded in the static binary. Markdown files remain external and can be updated without rebuilding.
Rootless Podman Quadlet (recommended)
This example builds on AMD64 Arch Linux and deploys to an ARM64 Raspberry Pi 5 running Debian 13 with systemd, reachable with ssh pi.
-
Install the build tools and Podman with ARM64 emulation on the local development machine:
sudo pacman -Syu --needed podman qemu-user-static qemu-user-static-binfmt binutils sudo systemctl restart systemd-binfmt.service mkdir -p ~/.config/containers/registries.conf.d printf '%s\n' 'unqualified-search-registries = ["docker.io"]' > ~/.config/containers/registries.conf.d/10-docker-io.conf podman run --rm --platform linux/arm64 docker.io/library/alpine:3.24 uname -mThe last command should print
aarch64. For rootless user setup, see ArchWiki: Podman. -
Connect with
ssh piand install Podman. Debian 13 provides Podman 5.4 with Quadlet:sudo apt update sudo apt install -y podman podman --version exit -
On the local machine, clone the project and its submodules, install dependencies, and build the frontend:
git clone --recurse-submodules https://github.com/crystal-china/website.git cd website build_scripts/bin/crystal_build_arm64_binary --deps-only # Install Shards dependencies into lib/. podman run --rm -v "$PWD:/app" -w /app docker.io/oven/bun:1 bun install --frozen-lockfile podman run --rm -v "$PWD:/app" -w /app docker.io/oven/bun:1 bun run prod -
The script uses
shards buildto cross-compile the static ARM64 application binary:build_scripts/bin/crystal_build_arm64_binary --release # Generates bin/crystal_china -
Pass
tasks.cras the first argument to usecrystal buildand generate the staticbin/tasksbinary:build_scripts/bin/crystal_build_arm64_binary ./tasks.cr -o bin/tasksbin/tasksis not included in the application image. It runs directly on the Pi, connecting to PostgreSQL through the pod's published port at127.0.0.1:5432.For an AMD64 target, replace the three build commands in steps 3-5 with:
build_scripts/bin/crystal_build_amd64_binary --deps-only build_scripts/bin/crystal_build_amd64_binary --release build_scripts/bin/crystal_build_amd64_binary ./tasks.cr -o bin/tasksARM64 emulation and its verification in step 1 can be skipped. The packaging command in step 6 automatically detects the binary's architecture.
-
Package and transfer the application image for Quadlet deployment.
Create a local container image containing
bin/crystal_china, timezone data and CA certificates:build_scripts/bin/crystal_package_image bin/crystal_china localhost/crystal-china:latestTransfer the image to the Pi's Podman storage over SSH:
podman save localhost/crystal-china:latest | ssh pi 'podman load'Alternatively, copy
bin/crystal_chinadirectly to the Pi without packaging an image. See Native binary deployment. -
Create the runtime directories on the Pi:
ssh pi 'mkdir -p ~/apps/crystal_china_production/bin ~/apps/crystal_china_production/public ~/apps/crystal_china_production/script ~/.config/containers/systemd'Copy these files from the local project directory to the Pi. The optional Patchy uploader is excluded:
scp -p bin/tasks pi:apps/crystal_china_production/bin/ scp -p script/wait-dependencies.sh pi:apps/crystal_china_production/script/ scp -pr public/markdowns pi:apps/crystal_china_production/public/ scp -p .env.sample pi:apps/crystal_china_production/ scp -p \ podman/containers/systemd/crystal_china.pod \ podman/containers/systemd/crystal_china_pg.container \ podman/containers/systemd/crystal_china_pg.env \ podman/containers/systemd/crystal_china_pgdata.volume \ podman/containers/systemd/crystal_china_redis.container \ podman/containers/systemd/crystal_china_web.container \ pi:.config/containers/systemd/ -
On the Pi, create
~/apps/crystal_china_production/.env, for example:LUCKY_ENV=production SECRET_KEY_BASE=REPLACE_WITH_A_NEW_RANDOM_SECRET HOST=0.0.0.0 PORT=5000 APP_DOMAIN=https://community.example.com ASSET_HOST= DATABASE_URL=postgres://postgres:CHANGE_ME@127.0.0.1:5432/crystal_china_production ADMIN_EMAILS=admin@example.com TZ=Asia/Shanghai CARBON_SMTP_SERVER=smtp.example.com CARBON_SMTP_PORT=587 CARBON_SMTP_USER=your-smtp-user CARBON_SMTP_PASSWORD=your-smtp-passwordFrom
~/apps/crystal_china_productionon the Pi, generate a newSECRET_KEY_BASEwithbin/tasks gen.secret_key.Replace the example values. The database password must match
POSTGRES_PASSWORDin~/.config/containers/systemd/crystal_china_pg.env. -
On the Pi, start the pod and initialize the database before allowing public access. PostgreSQL creates
crystal_china_productionautomatically fromPOSTGRES_DBon its first start:sudo loginctl enable-linger "$USER" systemctl --user daemon-reload systemctl --user start crystal_china-pod.service cd ~/apps/crystal_china_production script/wait-dependencies.sh # Wait up to 100 seconds bin/tasks db.migrate bin/tasks db.seed.required_data bin/tasks db.sync_doc_content -
Check that the pod and Web service are running:
systemctl --user status crystal_china-pod.service crystal_china_web.service --no-pager
The website listens at http://127.0.0.1:5000 on the Pi. Point an HTTPS reverse proxy or tunnel at it; production login cookies require HTTPS. The pod starts automatically after reboot. Logs: journalctl --user -u crystal_china_web.service -n 100 --no-pager.
PostgreSQL data lives in the crystal_china_pgdata volume. Uploads additionally require Patchy and adapting the upload domain in upload.cr.
Native binary deployment
Run the application directly on the Pi, keeping its dependencies in the Quadlet pod above.
-
Follow steps 1-5 above to build the application and task binaries. Skip image packaging and transfer in step 6.
-
Reuse steps 7-9 for runtime files, environment configuration and database initialization, with these changes:
Do not copy
crystal_china_web.containerto the Pi. Before starting the pod, removePublishPort=127.0.0.1:5000:5000from itscrystal_china.podfile so the host application can use port 5000. Keep the database port mapping.Set
HOST=127.0.0.1in the application's.envwhen using a reverse proxy on the same machine. -
On the local machine, copy the application binary to the Pi:
scp -p bin/crystal_china pi:apps/crystal_china_production/bin/ -
On the Pi, run the application from its deployment directory:
cd ~/apps/crystal_china_production bin/crystal_china
For background operation, use the systemd service example. Procodile is another option. The HTTPS reverse proxy setup is the same as above.
Updating documentation
- Edit files under
public/markdowns/and updatenavigation.ymlwhen the page should appear in the Sidebar and Pager. - Synchronize the directory with
rsync -a public/markdowns/ .../public/markdowns/. Manually delete obsolete.mdfiles from the server. - Run
bin/tasks db.sync_doc_contenton the server to update searchable content and remove deleted documents from search. No rebuild or restart is needed.
A Markdown file omitted from navigation.yml remains directly accessible and searchable, but it will not appear in the Sidebar or Pager. Visiting a document also syncs its content into the search index.
Project Notes
Contributing
- Fork it (https://github.com/zw963/website/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Billy.Zheng - creator and maintainer
website
- 5
- 1
- 2
- 0
- 20
- about 5 hours ago
- September 2, 2024
MIT License
Sun, 15 Nov 2026 12:43:27 GMT