Fix Docker builds, TS errors, and deploy config

- API: PHP 8.4, composer install --no-scripts + dump-autoload after COPY
- Admin: fix TS (Event.upload_password, unused router, api XSRF, window.open)
- Upload: Uppy v5 (hideProgressDetails, headers, destroy), unused watch, api XSRF
- Build script: loop over api/admin/upload, push :latest as well as VERSION
- Deploy: MySQL from docker.io, platform linux/amd64; README troubleshooting

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-03 14:00:09 +01:00
parent b492df3b1a
commit 65b41ba266
12 changed files with 53 additions and 32 deletions

View File

@@ -1,8 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const notification = ref<{ type: 'success' | 'error'; message: string } | null>(null)
onMounted(() => {

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import AdminLoading from '../components/AdminLoading.vue'
import { useEvents } from '../composables/useEvents'
import { api } from '../services/api'
@@ -8,7 +8,6 @@ import type { Event } from '../types/event'
import type { Upload } from '../types/upload'
const route = useRoute()
const router = useRouter()
const eventId = computed(() => Number(route.params.id))
const { fetchEvent } = useEvents()
@@ -37,6 +36,10 @@ function copyUploadUrl() {
alert('Upload URL copied to clipboard')
}
function openInNewTab(url: string) {
window.open(url, '_blank')
}
async function getDownloadUrl(upload: Upload) {
const { data } = await api.get<{ url: string }>(`/admin/uploads/${upload.id}/download-url`)
if (data.url) window.open(data.url, '_blank')
@@ -108,7 +111,7 @@ function statusBadge(status: string) {
<button
v-if="u.google_drive_web_link"
class="btn btn-outline-primary"
@click="window.open(u.google_drive_web_link!, '_blank')"
@click="openInNewTab(u.google_drive_web_link!)"
>
Open in Drive
</button>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import AdminLoading from '../components/AdminLoading.vue'
import { useEvents } from '../composables/useEvents'
const router = useRouter()
const { events, loading, pagination, fetchEvents, deleteEvent } = useEvents()
onMounted(() => fetchEvents())

View File

@@ -17,8 +17,9 @@ api.interceptors.request.use((config) => {
const token = document.cookie
.split('; ')
.find((row) => row.startsWith('XSRF-TOKEN='))
if (token) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(token.split('=')[1])
const value = token?.split('=')[1]
if (value) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(value)
}
return config
})

View File

@@ -12,6 +12,7 @@ export interface Event {
allowed_extensions: string[]
require_password: boolean
has_password: boolean
upload_password?: string | null
created_at: string
updated_at: string
uploads_count?: number

View File

@@ -1,5 +1,5 @@
# event-uploader API Laravel. Same image used for api and queue (different command).
FROM php:8.3-cli-alpine
FROM php:8.4-cli-alpine
# PHP extensions for Laravel + MySQL + Google etc.
RUN apk add --no-cache \
@@ -25,13 +25,16 @@ ENV COMPOSER_ALLOW_SUPERUSER=1
WORKDIR /app
# Dependencies first (better layer cache)
# Dependencies first (better layer cache). --no-scripts so Laravel's post-autoload-dump (artisan) is not run yet.
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
RUN composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
# Application
COPY . .
# Regenerate autoloader and run Laravel's post-install scripts (e.g. package:discover) now that artisan exists
RUN composer dump-autoload --optimize --no-dev
# .env and APP_KEY are provided at runtime via compose
# Writable dirs (runtime will mount or use defaults)

View File

@@ -43,3 +43,15 @@ After pushing, deploy on the server: set `TAG=1.0.0` in the stack `.env`, then i
- In Dockge: open the stack → **Pull** (to fetch new images from Gitea) → **Redeploy** (or Stop + Start).
- Or on the server: set `TAG=1.0.0` in `.env`, then `docker compose pull && docker compose up -d`.
## Troubleshooting: "manifest unknown" (e.g. for admin)
This means the registry has no image for the tag Dockge is using.
1. **Match the tag** In the stack `.env`, set `TAG` to the exact tag you pushed (e.g. the git hash from the build output, or `latest`). After running `./scripts/docker-build-push.sh`, the script prints the tag and now also pushes `:latest`, so `TAG=latest` works.
2. **Re-push all images** From project root run `./scripts/docker-build-push.sh` again so api, admin, and upload are all pushed with the same tag.
3. **Registry login on the server** Where Dockge runs, ensure `docker login 10.0.10.205:3000` has been done and that Docker has `10.0.10.205:3000` in `insecure-registries` (if using HTTP).
### "no matching manifest for linux/amd64" (MySQL)
The stack pins MySQL to `docker.io/library/mysql:8.0` with `platform: linux/amd64` so the server pulls the correct image from Docker Hub. If your Dockge server is ARM (e.g. Raspberry Pi), change the MySQL service in `docker-compose.yml` to `platform: linux/arm64`.

View File

@@ -66,7 +66,8 @@ services:
- event-uploader
mysql:
image: mysql:8.0
image: docker.io/library/mysql:8.0
platform: linux/amd64
ports:
- "3004:3306"
environment:

View File

@@ -22,18 +22,21 @@ echo "Building and pushing images with tag: $VERSION"
echo "Registry: $REGISTRY, Owner: $OWNER"
echo ""
docker build -t "$REGISTRY/$OWNER/event-uploader-api:$VERSION" "$ROOT/api"
docker push "$REGISTRY/$OWNER/event-uploader-api:$VERSION"
docker build -t "$REGISTRY/$OWNER/event-uploader-admin:$VERSION" "$ROOT/admin"
docker push "$REGISTRY/$OWNER/event-uploader-admin:$VERSION"
docker build -t "$REGISTRY/$OWNER/event-uploader-upload:$VERSION" "$ROOT/upload"
docker push "$REGISTRY/$OWNER/event-uploader-upload:$VERSION"
for name in api admin upload; do
docker build -t "$REGISTRY/$OWNER/event-uploader-$name:$VERSION" "$ROOT/$name"
docker push "$REGISTRY/$OWNER/event-uploader-$name:$VERSION"
if [ "$VERSION" != "latest" ]; then
docker tag "$REGISTRY/$OWNER/event-uploader-$name:$VERSION" "$REGISTRY/$OWNER/event-uploader-$name:latest"
docker push "$REGISTRY/$OWNER/event-uploader-$name:latest"
fi
done
echo ""
echo "Done. Images pushed:"
echo " $REGISTRY/$OWNER/event-uploader-api:$VERSION"
echo " $REGISTRY/$OWNER/event-uploader-admin:$VERSION"
echo " $REGISTRY/$OWNER/event-uploader-upload:$VERSION"
echo "On Dockge server: set TAG=$VERSION in stack .env, then Pull and Redeploy."
if [ "$VERSION" != "latest" ]; then
echo " (also tagged and pushed as :latest)"
fi
echo "On Dockge: set TAG=$VERSION (or TAG=latest) in stack .env, then Pull and Redeploy."

View File

@@ -35,7 +35,7 @@ onMounted(() => {
target: uppyContainer.value,
inline: true,
proudlyDisplayPoweredByUppy: false,
showProgressDetails: true,
hideProgressDetails: false,
height: 360,
theme: 'light',
})
@@ -49,7 +49,7 @@ onMounted(() => {
getResponseData() {
return {}
},
getRequestHeaders() {
headers: () => {
const headers: Record<string, string> = {}
const password = getPassword()
if (password) {
@@ -58,8 +58,9 @@ onMounted(() => {
const token = document.cookie
.split('; ')
.find((row) => row.startsWith('XSRF-TOKEN='))
if (token) {
headers['X-XSRF-TOKEN'] = decodeURIComponent(token.split('=')[1])
const value = token?.split('=')[1]
if (value) {
headers['X-XSRF-TOKEN'] = decodeURIComponent(value)
}
return headers
},
@@ -77,14 +78,14 @@ onMounted(() => {
onBeforeUnmount(() => {
if (successTimeout) clearTimeout(successTimeout)
uppy?.close()
uppy?.destroy()
uppy = null
})
watch(
() => props.slug,
() => {
uppy?.close()
uppy?.destroy()
uppy = null
}
)

View File

@@ -1,4 +1,4 @@
import { ref, watch, type Ref } from 'vue'
import { ref, type Ref } from 'vue'
import { api } from '../services/api'
import type { PublicEvent } from '../types/event'

View File

@@ -14,8 +14,9 @@ api.interceptors.request.use((config) => {
const token = document.cookie
.split('; ')
.find((row) => row.startsWith('XSRF-TOKEN='))
if (token) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(token.split('=')[1])
const value = token?.split('=')[1]
if (value) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(value)
}
return config
})