Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Version control
.git
.gitignore

# IDE
.vscode

# Dependencies (reinstalled inside container)
node_modules

# Mac-specific artefacts
.DS_Store

# Docker files (don't nest)
Dockerfile
docker-compose*.yml
.dockerignore

# Documentation
README.md
DOCKER-DEPLOY.md

# Logs & reports (mounted as volumes at runtime)
logs/
reports/

# Feedback historical (temporary, cleaned up by the script)
feedback-historical/

# Local environment file (injected at runtime)
.env
41 changes: 41 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Editor configuration — keeps whitespace consistent across all tools

root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 4

[*.{md,txt}]
indent_size = 4

[*.sh]
indent_style = space
indent_size = 2

[*.yml]
indent_style = space
indent_size = 2

[*.yaml]
indent_style = space
indent_size = 2

[*.{json,jsonc}]
indent_style = space
indent_size = 2

[*.ts]
indent_style = space
indent_size = 2

[*.js]
indent_style = space
indent_size = 2

[Makefile]
indent_style = tab
207 changes: 207 additions & 0 deletions DOCKER-DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# Docker Deployment to Synology NAS

This project can run entirely inside a lightweight Docker container on your Synology NAS via **Container Station** (the free Docker plugin for DSM). The entire pipeline runs inside an Alpine-based Bun container — no need for your Mac to be on.

## Quick Deploy (CLI — Recommended)

One deployment script, zero NAS CLI knowledge needed:

```bash
# One-time setup on your Mac
brew install --cask docker
brew install sshpass rsync

# Deploy
export NAS_IP=192.168.1.100
export NAS_USER=admin
export NAS_PASS=yourpassword
./scripts/deploy-nas.sh
```

This script:

1. Builds the Docker image locally on your Mac
2. Saves and SSH-transfers the image to your NAS
3. Loads the image on the NAS
4. Copies config files and source code
5. Starts the container with all volumes and env files mounted

## Manual Deployment via Container Station GUI

If you prefer the Container Station GUI instead of the CLI script:

1. **Transfer files via File Station:**
- Copy the entire project folder to `/docker/dailyreport/` on your NAS

2. **Build image via Container Station:**
- Open **Container Station** -> **Created Image** -> **Create from URL**
- Set URL to local path: `/docker/dailyreport/`
- Container Station will find the `Dockerfile` and build

3. **Create container:**
- Open **Container Station** -> **Container** -> **Create**
- Select image: `dailyreport:latest`
- Set scheduling: **Daily at 03:00**, task: **Start**
- Advanced settings:
- **Volume:** Mount `/volume1/docker/dailyreport/` -> `/app` (Read/Write)
- **Environment:** Add one variable `ENV_FILE_PATH` = `/app/.env`
- **Network:** Use `bridge` (default)
- Start the container

## Architecture

```
+-------------------------+ +----------------------------+
| Your Mac | | Synology NAS |
| | scp | |
| ./scripts/ | ----------> | /volume1/docker/dailyreport|
| deploy-nas.sh -------> | | +-- reports/ (persisted) |
| | rsync | +-- config/ (persisted) |
| local docker build | | +-- logs/ (persisted) |
| + image save | | +-- src/ (application) |
| | | |
| | | Container Station |
| | | +--------------------+ |
| | | | dailyreport_daily | |
| | | | oven/bun:1-alpine | |
| | | | bun run generate | |
| | | +--------------------+ |
| | | ^ |
| | | scheduled task |
+-------------------------+ +----------------------------+
|
| SFTP
v
+-----------------+
| IONOS SFTP |
| (web UI host) |
+-----------------+
```

## File Persistence

All state persists across container recreations via bind mounts:

| Path (inside container) | NAS Host Path | Purpose |
|---|---|---|
| `/app/reports/` | `/volume1/docker/dailyreport/reports/` | Generated Markdown reports (YYYY-MM-DD.md) |
| `/app/config/` | `/volume1/docker/dailyreport/config/` | interests.yaml, seen-urls.json, feedback-weights.json, blacklist.json |
| `/app/logs/` | `/volume1/docker/dailyreport/logs/` | dailyreport.log, access.log |
| `/app/.env` | `/volume1/docker/dailyreport/.env` | SFTP and OAuth credentials (DO NOT commit to git) |
| `/app/` | `/volume1/docker/dailyreport/` | Application source code |

## Managing the Container

### View logs

```bash
ssh admin@192.168.1.100
docker logs dailyreport_daily --tail 50
```

Follow in real time:

```bash
docker logs -f dailyreport_daily
```

### View generated reports

```bash
ssh admin@192.168.1.100
ls /volume1/docker/dailyreport/reports/
cat /volume1/docker/dailyreport/reports/2025-05-08.md
```

### Re-deploy after code changes

```bash
export NAS_IP=192.168.1.100
export NAS_USER=admin
export NAS_PASS=yourpassword
./scripts/deploy-nas.sh
```
Stops old, builds new, starts fresh container.

### Manual full rebuild

```bash
ssh admin@192.168.1.100
docker stop dailyreport_daily
docker rm dailyreport_daily
docker rmi dailyreport:latest
./scripts/deploy-nas.sh
```

## Scheduling via Container Station (One-Time Setup)

The container exits after completing its task. For it to run daily, you need a **Scheduled Task**:

1. Open DSM -> **Container Station**
2. Click **Scheduled Task** in the left sidebar
3. Click **Create**
4. Select container: **dailyreport_daily**
5. Schedule: **Every day** at **03:00**
6. Task: **Start**
7. Save

The container will start at 03:00, run for ~3-5 seconds, then exit. Container Station restarts it at the next daily trigger.

## Troubleshooting

### Container exits immediately with an error

```bash
ssh admin@192.168.1.100
docker logs dailyreport_daily --tail 50
```

Common causes: missing `.env` on NAS, wrong SFTP credentials, NAS cannot reach the internet.

### Cannot reach SFTP host from NAS

The NAS needs outbound HTTPS and SFTP access:

```bash
ssh admin@192.168.1.100
curl -Is https://google.com
curl -Is https://home554762802.1and1-data.host
```

### .env not being read by the container

Verify the `.env` file exists on the NAS:

```bash
ssh admin@192.168.1.100
cat /volume1/docker/dailyreport/.env
```

It must have the FTP variables: `FTP_HOST`, `FTP_USER`, `FTP_PASS`, optionally `TARGET_DIR`.

### Container reports "image not found"

```bash
ssh admin@192.168.1.100
docker images | grep dailyreport
```

If missing, re-run `./scripts/deploy-nas.sh`.

### Logs show permission errors

The `Dockerfile` creates an `appuser` (UID 1000) with proper ownership. If your NAS has different UID/GID mappings, you may need to adjust the Dockerfile's `USER` directive or set appropriate file permissions on the NAS host directories.

## What is Inside the Container

| Path | Purpose |
|---|---|
| `/app/` | Application root directory |
| `/app/src/` | TypeScript source files |
| `/app/src/fetchers/` | HN, Reddit, RSS fetcher modules |
| `/app/config/` | `interests.yaml`, `seen-urls.json`, `feedback-weights.json` |
| `/app/reports/` | Generated Markdown reports (YYYY-MM-DD.md) |
| `/app/logs/` | `dailyreport.log`, `access.log` |
| `/app/node_modules/` | Production dependencies (fast-xml-parser, js-yaml, ssh2-sftp-client) |

The container is Alpine-based (~80 MB image) running Bun under a non-root user for security.
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ============================================================
# DailyReport — Dockerfile
# ============================================================
# Built for Synology Container Station and any standard Docker
# runtime. Alpine-based Bun image keeps the image ~80 MB.
# ============================================================

FROM oven/bun:1-alpine

# Install git (needed for feedback git commit workflow)
# and curl (useful for health checks or the deploy script)
RUN apk add --no-cache git curl

# Create non-root user for security
RUN addgroup -g 1000 appgroup && \
adduser -u 1000 -G appgroup -D appuser

WORKDIR /app

# --- Dependency install (layer-cached: changes when these change) ---
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production

# --- Application source ---
COPY . .

# Create runtime directories with correct ownership
RUN mkdir -p /app/reports /app/logs /app/config && \
chown -R appuser:appgroup /app

# Switch to non-root user
USER appuser

# Default: run the curation pipeline (one-shot, exits when done)
CMD ["bun", "run", "generate"]
Loading