EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Web / deploy Laravel แล้วเจอ 500 ชั่วครู่: แคชถูกลบก่อนเขียนใหม่
Web

deploy Laravel แล้วเจอ 500 ชั่วครู่: แคชถูกลบก่อนเขียนใหม่

โดย Ton-Or ·11 พ.ย. 2026·อ่าน 11 นาที
deploy Laravel แล้วเจอ 500 ชั่วครู่: แคชถูกลบก่อนเขียนใหม่

ข้อผิดพลาดที่เกิดเสี้ยววินาทีตอน deploy ไม่ได้อยู่ในโค้ดของเรา มันอยู่ในลำดับขั้นของคำสั่งแคช ลบก่อนเขียนใหม่ แปลว่ามีช่วงที่ไม่มีไฟล์ให้อ่านเลย

สรุปใน 1 นาที

อาการ/ปัญหา
ตอน deploy สั่ง view:cache, config:cache, route:cache แล้วคำขอจริงเจอ 500 ช่วงสั้น ๆ
ใครควรอ่าน
คนที่ deploy Laravel บนเครื่องที่มีผู้ใช้เข้าอยู่ตลอด
ทางแก้แบบสั้น
warm เฉพาะ template ที่หมดอายุแทนการลบทั้งหมด และสร้างแคช config กับ route ไว้ข้าง ๆ แล้ว rename ทับแบบอะตอมมิก
ข้ามไปขั้นตอน

พอดีผมเปิดล็อก production ของเว็บ Laravel ของลูกค้ารายหนึ่ง แล้วเจอ 500 โผล่มาสองสามบรรทัดตรงช่วงที่ผม deploy พอดี หน้าเว็บกลับมาปกติเองหลังจากนั้น ไม่มีใครแจ้งว่าเข้าไม่ได้ แต่ล็อกบอกว่ามีคำขอจริงของคนจริงที่โดนเข้าไป ข้อความแรกคือ filemtime(): stat failed ตอนผมรัน view:cache และอีกข้อความคือ require(bootstrap/cache/routes-v7.php): Failed to open stream ตอนรัน route:cache ต้นเหตุไม่ใช่โค้ดที่ผมเขียน แต่เป็นลำดับขั้นของคำสั่งแคชของ Laravel เอง บทความนี้เล่าว่าผมไล่เจอได้อย่างไร และวิธี deploy ที่ไม่มีช่องว่างที่ผมทำขึ้นมาแทน ชื่อโฟลเดอร์ ชื่อสคริปต์ และ endpoint ของระบบคิวในบทความผมเปลี่ยนหรือตัดออกหมด เหลือแต่ลำดับเหตุการณ์ที่เป็นของจริง

เรื่องนี้เกิดกับทุกเว็บที่ deploy ขณะมีคนใช้งาน

ถ้าเว็บของคุณมีผู้ใช้ตลอดเวลา ทุกคำสั่งที่ "ลบแล้วสร้างใหม่" ในช่วง deploy ล้วนเป็นช่องว่างที่รอให้มีคำขอโดนเข้าไป ยิ่งเว็บคนเข้าเยอะ ช่องว่างสั้น ๆ ก็ยิ่งมีคำขอตกลงไปในนั้นแน่

อาการสองแบบ ที่ดูเหมือนคนละเรื่อง

อาการแรกมาจากแคชของ Blade คำสั่ง php artisan view:cache ทำงานสองจังหวะ จังหวะแรกเรียก view:clear ลบไฟล์ที่คอมไพล์แล้วทั้งหมด จังหวะที่สองไล่คอมไพล์ template ทุกไฟล์กลับมาใหม่ ระหว่างสองจังหวะนั้น ถ้ามีคำขอเข้ามาและต้องเรนเดอร์หน้า Laravel จะไปหาไฟล์ที่คอมไพล์แล้ว ไม่เจอ แล้วคอมไพล์เองเฉพาะตอนนั้น แต่ถ้าสองกระบวนการแย่งกันเขียนไฟล์เดียวกัน หรือไฟล์ถูกลบระหว่างที่อีกกระบวนการกำลัง stat อยู่ ก็ได้ filemtime(): stat failed ผมเจอข้อความนี้ในล็อก production

อาการที่สองมาจากแคช config และ route สองคำสั่งนี้เหมือนกันตรงที่เรียก clear ก่อนแล้วค่อยเขียนไฟล์ใหม่ ถ้ามีคำขอเข้ามาตอนไฟล์ routes-v7.php ถูกลบไปแล้วแต่ยังเขียนใหม่ไม่เสร็จ ตัว bootstrap ของ Laravel ที่เช็กว่าแคช route มีอยู่ไหมกับตอนที่ require จริงอาจเห็นไม่ตรงกัน ได้ Failed to open stream กลายเป็น 500 ผมเจอแบบนี้ในการ deploy สองครั้ง

view:cache

ลบทั้งหมดแล้วคอมไพล์ใหม่

ช่วงที่ลบเสร็จแต่ยังคอมไพล์ไม่ครบ คือช่วงที่ไม่มีไฟล์พร้อมใช้ เวลายาวตามจำนวน template ของเว็บ ยิ่งเว็บใหญ่ช่องว่างยิ่งกว้าง

config:cache / route:cache

ลบไฟล์เก่าแล้วเขียนไฟล์ใหม่

ช่องว่างสั้นกว่ามาก แต่ไม่ใช่ศูนย์ แค่ต้องมีคำขอเข้ามาตรงนั้นพอดี ก็ได้ 500 ที่เกิดแล้วหายไปเอง แบบที่หาสาเหตุยากที่สุด

สรุปคือสองอาการนี้มีรากเดียวกัน คือคำสั่งที่ "ทำลายของเก่าก่อน แล้วค่อยสร้างของใหม่" ผมเลยตัดสินใจแก้ที่หลักการ ไม่แก้ทีละคำสั่ง เป้าหมายคือไฟล์ที่คำขอต้องอ่านต้องมีให้อ่านเสมอ ไม่ว่าเวลาไหน

ก่อน: view:cacheไฟล์เดิมใช้ได้ช่องว่างคอมไพล์ใหม่ครบคำขอเจอ 500หลัง: view-warmไฟล์เดิมยังอยู่ทุกไฟล์ ไม่ลบอะไรเปลี่ยนเฉพาะที่หมดอายุเวลาในการ deploy
แถวบนมีช่วงสีแดงที่ไม่มีไฟล์ให้อ่าน แถวล่างไม่ลบไฟล์เลย ทุกคำขอเจอไฟล์ที่คอมไพล์แล้วเสมอ

กับดักที่มักสับสนกัน: template เก่าไม่ยอมเปลี่ยน

ก่อนไปที่วิธีแก้ ขอแยกอีกกับดักหนึ่งที่หน้าตาคล้ายกันแต่คนละเรื่อง คือ deploy ไปแล้วหน้าเว็บยังเสิร์ฟ HTML เก่าไม่หยุด Blade จะคอมไพล์ template ใหม่ก็ต่อเมื่อ mtime ของไฟล์ต้นทางใหม่กว่าไฟล์ที่คอมไพล์แล้ว (เป็นเรื่องของ isExpired) ถ้าแตกไฟล์จากอาร์ไคฟ์โดยรักษา mtime เดิม ไฟล์ต้นทางอาจ "เก่ากว่า" ของที่คอมไพล์ไว้ มันก็ไม่ถูกคอมไพล์ใหม่ ทางแก้คือแตกด้วยตัวเลือกที่ตั้ง mtime เป็นเวลาปัจจุบัน (tar --touch) หรือ touch ไฟล์หลังแตก

# แตกไฟล์โดยไม่รักษา mtime เก่า
tar --touch -xzf release.tgz -C /path/to/app

# กรณีฉุกเฉิน: ทำให้ทุก template ใหม่กว่าของที่คอมไพล์
find resources/views -name '*.blade.php' -exec touch {} +

ส่วนนี้ผมยืนยันจากโน้ตเก่าของผมเอง ไม่ได้อยู่ในสคริปต์บนเครื่องตอนนี้ เลยเล่าเป็นประวัติที่เคยสังเกตพบเท่านั้น และถ้าอาการของคุณคือ "deploy แล้วหน้าเก่า" เฉย ๆ โดยไม่มี 500 ผมรวมการไล่ทีละชั้นไว้ใน deploy แล้วหน้าเก่าไม่เปลี่ยน ไล่แคชทีละชั้น ส่วนบทความนี้เป็นอีกเรื่อง คือแคช "สร้างใหม่" แล้วทำให้เว็บล้ม

แก้แคช view: คอมไพล์เฉพาะที่หมดอายุ และไม่ลบอะไรเลย

หลักของผมสั้นมาก ถ้า template ยังไม่หมดอายุ ก็ไม่ต้องแตะ และห้ามลบไฟล์ที่คอมไพล์ไว้ คำสั่ง app:view-warm ของผมเลยทำสิ่งเดียว คือไล่ template ทุกไฟล์ ถามคอมไพเลอร์ว่าไฟล์นี้หมดอายุไหม ถ้าใช่ก็คอมไพล์ใหม่ทับ ถ้าไม่ก็ปล่อยผ่าน ในโค้ดจริงผมให้คำสั่งนี้ extends ViewCacheCommand เพื่อใช้ของเดิม ส่วนที่แสดงข้างล่างเป็นแบบย่อที่เขียนใหม่ให้อ่านง่าย

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Symfony\Component\Finder\Finder;

class ViewWarm extends Command
{
    protected $signature = 'app:view-warm';
    protected $description = 'Compile only expired Blade templates; never delete compiled files';

    public function handle(): int
    {
        $compiler = app('blade.compiler');
        $compiled = 0;

        foreach ((new Finder)->in(resource_path('views'))->files()->name('*.blade.php') as $file) {
            $path = $file->getRealPath();
            if ($compiler->isExpired($path)) {   // mtime ต้นทางใหม่กว่าไฟล์คอมไพล์ หรือยังไม่มีไฟล์คอมไพล์
                $compiler->compile($path);       // เขียนทับ ไม่ลบก่อน
                $compiled++;
            }
        }

        $this->info("compiled $compiled expired template(s)");
        return self::SUCCESS;
    }
}

สิ่งที่ได้คือ ทุกคำขอระหว่าง deploy เจอไฟล์ที่คอมไพล์แล้วเสมอ จะเป็นเวอร์ชันเก่าหรือใหม่ก็ได้ ไม่ใช่ "ไม่มี" และไฟล์ใหม่ถูกเขียนทีละไฟล์ ไม่ได้ลบทั้งโฟลเดอร์ ถ้าผมยิงโหลดใส่หน้าหลักขณะรัน view:cache แล้วนับ 500 เทียบกับขณะรัน view-warm ผลต่างก็ควรเห็นชัด ตามเหตุผลข้างบน

ร้านค้าที่เปลี่ยนป้ายหน้าร้านด้วยการทำป้ายใหม่ไว้ในห้องหลังร้านแล้วสลับเข้าไปในครั้งเดียว แทนที่จะรื้อป้ายเก่าลงก่อน
ทำป้ายใหม่ให้เสร็จหลังร้าน แล้วค่อยสลับ หน้าร้านไม่เคยมีช่วงที่ไม่มีป้าย

แก้แคช config และ route: สร้างไว้ข้าง ๆ แล้ว rename ทับ

แคช config กับ route ผมใช้ท่าเดียวกับการเขียนไฟล์ให้ปลอดภัย คือไม่เขียนทับไฟล์จริงตรง ๆ แต่ให้คำสั่งสร้างไว้ในไฟล์ชั่วคราวข้างไฟล์จริง แล้ว rename() ทับ ซึ่งเป็นการกระทำเดียวที่ทำให้ไฟล์จริงเปลี่ยนจากของเก่าเป็นของใหม่ในพริบตา (อะตอมมิกบนไฟล์ซิสเต็มเดียวกัน) กลไกคือ Laravel อ่านตำแหน่งไฟล์แคชจากตัวแปรสภาพแวดล้อม APP_CONFIG_CACHE, APP_ROUTES_CACHE และ APP_EVENTS_CACHE ผมเลยรันคำสั่งแคชแต่ละตัวใน child process โดยตั้งตัวแปรเหล่านี้ให้ชี้ไฟล์ชั่วคราว ขั้นตอน clear ในคำสั่งก็ลบแค่ไฟล์ชั่วคราว ไม่แตะไฟล์จริง

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;

class CacheSwap extends Command
{
    protected $signature = 'app:cache-swap';
    protected $description = 'Rebuild config/routes/events caches without a missing-file gap';

    public function handle(): int
    {
        // ลำดับ: config -> routes -> events
        $jobs = [
            ['config:cache', 'APP_CONFIG_CACHE', app()->getCachedConfigPath()],
            ['route:cache',  'APP_ROUTES_CACHE', app()->getCachedRoutesPath()],
            ['event:cache',  'APP_EVENTS_CACHE', app()->getCachedEventsPath()],
        ];

        foreach ($jobs as [$cmd, $envKey, $live]) {
            if (! $this->swap($cmd, $envKey, $live)) {
                $this->error("swap failed: $cmd");
                return self::FAILURE;
            }
        }
        return self::SUCCESS;
    }

    private function swap(string $cmd, string $envKey, string $live): bool
    {
        $tmp = preg_replace('/\.php$/', '', $live) . '.swap-' . getmypid() . '.php';

        $r = Process::env([$envKey => $tmp])->run([PHP_BINARY, 'artisan', $cmd]);

        if ($r->successful() && is_file($tmp)) {
            return rename($tmp, $live);          // อะตอมมิก: ไฟล์จริงไม่เคยหายไป
        }
        @unlink($tmp);                           // ล้ม: ทิ้งไฟล์ชั่วคราว ไฟล์จริงยังเป็นของเดิม
        return false;
    }
}

ข้อควรระวังคือ ไฟล์ชั่วคราวต้องอยู่โฟลเดอร์เดียวกับไฟล์จริง เพราะ rename ข้ามไฟล์ซิสเต็มไม่ใช่อะตอมมิก และถ้าคำสั่งแคชล้ม ไฟล์ชั่วคราวถูกลบ ไฟล์จริงยังเป็นของเดิมทุกประการ คืนค่า FAILURE ให้สคริปต์ deploy รู้ ส่วนเรื่อง opcache ผมเรียก opcache_invalidate ได้เฉพาะฝั่ง CLI ส่วน PHP-FPM จะเห็นไฟล์ใหม่ตาม mtime เมื่อเปิด validate_timestamps ไว้ เลยต้องรอสักพักก่อนทดสอบ ผมตั้งไว้ราว 60 วินาที

cache-swap: สร้างข้าง ๆ แล้วสลับในคำสั่งเดียวchild processroute:cacheAPP_ROUTES_CACHEชี้ไฟล์ .swap-pidrename()ทับไฟล์จริงทำตามลำดับconfig routes eventsรอ opcacheราว 60 วินาทีsmoke testหน้าแรก + route ที่แตะผ่านทุกข้อปล่อยจริงเจอ 5xxrollback ไฟล์และแคชไฟล์จริงถูกแทนที่ในคำสั่งเดียว ไม่มีเสี้ยววินาทีที่ไฟล์หาย
เส้นทางหลักอยู่แถวบน แถวล่างคือการตรวจ ถ้ามี 5xx แม้แต่ครั้งเดียวระบบย้อนไฟล์และแคชอัตโนมัติ

มีกติกาที่ผมเขียนไว้ในโค้ดและยึดตามเสมอ ห้ามรัน optimize:clear บน production เพราะการล้างแคช config เคยทำให้เว็บล่มมาแล้ว

ทางเลือกอื่นที่ผมชั่งแล้ว

ก่อนเขียนสองคำสั่งนี้ ผมไล่ดูทางเลือกที่คนส่วนใหญ่ใช้กันก่อน ทางแรกคือเปิดโหมดปิดปรุงด้วย php artisan down ระหว่าง deploy ทางนี้ง่ายและปลอดภัยที่สุดในแง่ไม่มีใครเจอ 500 แต่คนเข้าเว็บจะเจอหน้า 503 แทน ซึ่งสำหรับเว็บลูกค้าที่ต้องเปิดตลอดผมไม่ชอบ ทางที่สองคือ deploy แบบสร้างโฟลเดอร์ release ใหม่ทั้งชุดแล้วสลับ symlink ซึ่งเป็นวิธีที่ดีมากและแก้ปัญหานี้ได้ครบ แต่ต้องปรับโครงสร้างโฟลเดอร์และสคริปต์ deploy ทั้งชุด ซึ่งงานเยอะกว่ามาก ผมเลยเลือกแก้ในระดับคำสั่งก่อน

down / 503

ปลอดภัยแต่ผู้ใช้เห็นหน้าปิดปรุง

ไม่มีใครเจอ 500 แต่มีช่วงที่เว็บไม่ให้บริการเลย เหมาะกับงานที่ยอมปิดได้ ไม่เหมาะกับเว็บที่ต้องเปิดตลอด

warm + swap

เว็บเปิดตลอด ไม่มีช่องว่าง

ไม่ปิดเว็บ ไม่ต้องรื้อโครงสร้างโฟลเดอร์ ราคาที่จ่ายคือต้องเขียนและดูแลคำสั่งเพิ่มสองตัว กับต้องรู้เรื่อง opcache ให้ดี

ส่วนตัวผมมองว่าถ้าโครงสร้างโฮสต์เปิดให้ทำ symlink สลับ release ก็ควรทำทางนั้นก่อน เพราะแก้ครบทั้งโค้ด แคช และ vendor ในจังหวะเดียว สองคำสั่งของผมคือทางลัดสำหรับที่ที่ยังไม่พร้อมรื้อโครงสร้าง ตามความเหมาะสมของแต่ละโฮสต์ ไม่มีวิธีไหนถูกที่สุดสำหรับทุกที่

ลำดับ deploy ทั้งสาย ผ่านคิวที่ cron ดึงทุกนาที

การ deploy ของผมไม่ได้เป็นสคริปต์เดียว มันเป็นคิวของงานที่ cron ดึงมาทำทุกนาที แต่ละงานผ่านด่านเรียงตามนี้ ผมจัดลำดับให้ด่านที่เสียหายน้อยที่สุดมาก่อน

  1. เช็กความขัดแย้ง เทียบ sha256 ของไฟล์จริงบนเซิร์ฟเวอร์กับ sha256 ฐานที่ผู้ส่งเห็นตอนแก้ ถ้าไม่ตรงแปลว่ามีคนแก้ไฟล์นี้ไปก่อน งานนี้ไม่ผ่าน
  2. lint ด้วย php -l, คอมไพล์ Blade แล้ว php -l ผลที่ได้ และตรวจ JSON
  3. ปฏิเสธโค้ดที่เพิ่มการเรียก exec, eval หรือ Process ใหม่ ต้องให้คนดูเอง
  4. สำรองไฟล์จริงก่อนเขียนทับ
  5. เขียนไฟล์ใหม่ ถ้ามี migration ก็ dump ฐานข้อมูลก่อนแล้วรัน migrate --force
  6. รัน app:cache-swap แล้ว app:view-warm
  7. รอ opcache แล้ว smoke test: GET หน้าแรก หน้าตัวอย่าง route ที่ไฟล์นี้แตะ และ URL ที่ผู้ส่งระบุ
  8. เจอ 5xx แม้ครั้งเดียว rollback อัตโนมัติทั้งไฟล์และแคช

สังเกตว่า migration ไม่ถูก undo อัตโนมัติ เพราะย้อน schema บนข้อมูลจริงอันตรายกว่า ผมรายงานตำแหน่งไฟล์ dump แทนให้คนตัดสินใจเอง

บทเรียนข้อสุดท้าย: migration รันก่อนแคช config ใหม่

ในลำดับข้างบน migration รันก่อนขั้นสร้างแคช config ใหม่ ซึ่งมีผลข้างเคียงที่ต้องรู้ migration ตัวหนึ่งของผมเรียก Artisan::call เพื่อสั่ง build ดัชนีความรู้ขึ้นมาเลยตอนมิเกรต พอคำสั่งนั้นรันก่อนแคช config ใหม่ มันอาจเห็นค่า config เก่า ผมยืนยันได้จากลำดับขั้นและจากที่ผมห่อการเรียกไว้ด้วย try/catch กับ report() ในมิเกรชันแล้ว แต่กรณีล้มจริงที่เกิดมาจากโน้ตเก่า ผมไม่ได้เก็บล็อกไว้

// ในไฟล์ migration: ห้ามให้การ backfill ทำให้ migration ล้ม
public function up(): void
{
    Schema::table('example_items', function (Blueprint $table) {
        $table->string('locale', 5)->default('th')->after('id');
    });

    try {
        Artisan::call('app:build-index');   // เป็นแค่ความพยายาม ไม่ใช่เงื่อนไขของ schema
    } catch (\Throwable $e) {
        report($e);                          // บันทึกไว้ แต่ไม่ทำให้ migration ล้ม
    }
}

ผมแก้ด้วยสองอย่าง ห่อด้วย try/catch ให้ migration ไม่ล้มเพราะดัชนีสร้างไม่ได้ ดัชนีจะเติมเองภายหลังจาก model event และรันคำสั่งซ้ำด้วยมือได้เสมอ แต่ข้อที่ผมถือเป็นบทเรียนจริง ๆ คือ อย่าทำ data backfill ภายใน migration เด็ดขาด ให้ migration ทำแค่ schema แล้วเอางานเติมข้อมูลไปเป็นคำสั่งแยกที่รันหลัง deploy เมื่อแคชใหม่ถูกสลับเรียบร้อยแล้ว

ไม้วิ่งผลัดที่ส่งต่อจากมือหนึ่งไปอีกมือหนึ่ง โดยไม่มีช่วงใดที่ไม่มีใครถือไม้
ไม้ผลัดมีคนถือตลอด ไฟล์แคชก็ควรมีของให้คำขออ่านตลอดเช่นกัน แม้ในวินาทีที่กำลังสลับ
0ไฟล์ที่ถูกลบ
1 renameต่อไฟล์แคชจริง
60 วินาทีรอ opcache
3 ชนิดconfig routes events

ทดสอบให้เห็นกับตา

สิ่งที่ผมอยากทำเสมอกับการแก้แบบนี้คือเห็นความต่างด้วยตาตัวเอง ไม่ใช่เชื่อว่ามันดีขึ้นเพราะเหตุผลฟังขึ้น ผมใช้เครื่องมือยิงโหลดอย่าง ab หรือ hey ใส่หน้าหลักค้างไว้ขณะรันคำสั่ง แล้วนับจำนวน 500 ที่กลับมา

# เทอร์มินัล A: ยิงโหลดค้างไว้ แล้วนับ status ที่ไม่ใช่ 200
hey -z 60s -c 20 https://staging.example.com/ | grep -E "Status code|\[5"

# เทอร์มินัล B: รันทีละแบบในช่วงเวลานั้น เปรียบเทียบสองรอบ
php artisan view:cache        # รอบที่ 1 (แบบเดิม)
php artisan app:view-warm     # รอบที่ 2 (แบบใหม่)

# ดูล็อกแอปว่ามี stat failed / Failed to open stream ไหม
grep -E "stat failed|Failed to open stream" storage/logs/laravel.log | tail -n 20

ผมไม่ใส่ตัวเลขผลที่วัดได้ในบทความนี้ เพราะเว็บแต่ละที่ต่างกัน ทั้งจำนวน template ความเร็วดิสก์ และจำนวนคนเข้า สิ่งที่ควรเห็นคือรอบแรกมีโอกาสได้ 5xx ส่วนรอบที่สองไม่ควรได้เลย ส่วนตัวผมถือว่าถ้ารอบที่สองยังได้ 5xx แสดงว่ามีอย่างอื่นนอกเหนือจากช่องว่างของแคชที่ผิดอยู่ ซึ่งควรไล่ต่อด้วยล็อกก่อนจะสรุปอะไร

เรื่องที่ยังเหลือคือผมอยากให้ smoke test ตรวจจำนวน template ที่ถูกคอมไพล์ทับเปรียบเทียบกับจำนวนไฟล์ที่เปลี่ยนในงานนั้น เพื่อจับกรณีไฟล์ที่เปลี่ยนแล้วแต่ mtime เก่าได้ทันที ตรงไหนที่ผมเล่าไม่ครบหรือพลาด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย